#include "swagme.h"
#include "config.h"
#include "Game.h"
#include "Display.h"
#include "Player.h"
#include "Npc.h"
#include "Scene.h"
#include "Swag.h"

#include <iostream>
using std::cout;
using std::cerr;
using std::endl;

// 2 minutes for each screen (for now)
#define TIME_LEFT (2*60)

Game::Game() : 
    mTimeLeftInSec(TIME_LEFT), 
    mStartTimeInMs(SDL_GetTicks()), 
    mpDisplay(new Display()),
    mpPlayer(NULL),
    mpConfig(InitConfig("resources/wad.xml")),
    mpCurLevel(NULL)
{
  if(!mpDisplay) {
    cerr << "Error!  Could not initialize Display" << endl;
    exit(4);
  }
  cout << "Game and Display initialized." << endl;
  Init();
}

Game::~Game()
{
  // wipe this collection clean
  mRenderables.clear();

  if (mColMap.map) delete[] mColMap.map;

  for (int idx = 0; idx < mSwaggage.size(); idx++) {
    delete mSwaggage[idx];
  }
  mSwaggage.clear();

  for (int idx = 0; idx < mNonplayers.size(); idx++) {
    delete mNonplayers[idx];
  }
  mNonplayers.clear();

  for( map<string,Sprite*>::iterator spriteIt = mSprites.begin(); spriteIt != mSprites.end(); ++spriteIt) {
    delete spriteIt->second;
  }
  mSprites.clear();

  if(mpPlayer) {
    delete mpPlayer;
    mpPlayer = NULL;
  }

  if(mpConfig) {
    delete mpConfig;
    mpConfig = NULL;
  }

  if(mpScene) {
    delete mpScene;
    mpScene = NULL;
  }

  if(mpDisplay) {
    delete mpDisplay;
    mpDisplay = NULL;
  }

  cout << "Game and Display destroyed." << endl;
}

// read in all resources
// find the first conference
// set up player position
void Game::Init()
{
  InitSprites();

  // get all levels (order matters in the XML doc)
  int numLevels = mpConfig->getElementsByTagName("conference", &mAllLevels);
  if(!numLevels) {
    cerr << "Error!  Could not find any <conference> elements in wad.xml" << endl;
    exit(2000);
  }

  // find the first conference
  mpCurLevel = mAllLevels[0];
  if(!mpCurLevel) {
    cerr << "Error!  No suitable levels found (check order attributes on <conference> elements)" << endl;
    exit(-1);
  }
  int lev_width = ATOI(mpCurLevel->attr["width"]);
  int lev_height = ATOI(mpCurLevel->attr["height"]);
  cout << "First level was '" << mpCurLevel->attr["name"] << "'" << " (" << lev_width << "," << lev_height << ")" << endl;

  // set up background map
  mpScene = new Scene(mpCurLevel->attr["background"].c_str(), mpCurLevel->attr["collisionMap"].c_str(), mpCurLevel->attr["floor"].c_str());
  if(!mpScene) {
    cerr << "Error!  Could not initialize scene" << endl;
    exit(5);
  }
  
  // set up collision map in InitScene later
  
  // read in swag from <conference> element
  NodeArray swagNodes;
  mpCurLevel->getElementsByTagName("swag", &swagNodes);
  for(NodeIter swagIt = swagNodes.begin(); swagIt != swagNodes.end(); ++swagIt) {
    XMLNode* node = *swagIt;
    cout << "Found <swag> with type=" << node->attr["type"] << endl;

    int pts = 10;
    if(node->attr["type"] == "PEN") { pts = 8; }
    else if(node->attr["type"] == "HAT") { pts = 12; }
    else if(node->attr["type"] == "TSHIRT") { pts = 20; }
    else {
      cerr << "Warning!  Ignoring unknown swag type '" << node->attr["type"] << "'" << endl;
      continue;
    }

    if(mSprites.find(node->attr["sprite"]) == mSprites.end()) {
      cerr << "Error!  Could not find sprite '" << mSprites[node->attr["sprite"]] << "'" << endl;
      exit(-1);
    }

    int x = ATOI(node->attr["x"]);
    int y = ATOI(node->attr["y"]);
    if(node->attr["x"] == "RANDOM") {
      x = random(30, lev_width-30);
    }
    if(node->attr["y"] == "RANDOM") {
      y = random(30, lev_width-30);
    }

    Swag* aSwag = new Swag(mSprites[node->attr["sprite"]], x, y, pts );
    mSwaggage.push_back( aSwag ); // owned
    mRenderables.push_back( aSwag ); // not owned
    // TO DO: add to colmap, not owned
  }

  // read in npcs from <conference> element
  NodeArray npcNodes;
  mpCurLevel->getElementsByTagName("npc", &npcNodes);
  for(NodeIter npcIt = npcNodes.begin(); npcIt != npcNodes.end(); ++npcIt) {
    XMLNode* node = *npcIt;
    cout << "Found <npc> with type=" << node->attr["type"] << endl;

    if( (node->attr["type"] != "BABE") && (node->attr["type"] != "NERD") ) {
      cerr << "Warning!  Ignoring unknown NPC type '" << node->attr["type"] << "'" << endl;
      continue;
    }

    if(mSprites.find(node->attr["sprite"]) == mSprites.end()) {
      cerr << "Error!  Could not find sprite '" << mSprites[node->attr["sprite"]] << "'" << endl;
      exit(-1);
    }

    int x = ATOI(node->attr["x"]);
    int y = ATOI(node->attr["y"]);
    if(node->attr["x"] == "RANDOM") {
      x = random(30, lev_width-30);
    }
    if(node->attr["y"] == "RANDOM") {
      y = random(30, lev_width-30);
    }

    Npc* aNpc = new Npc(mSprites[node->attr["sprite"]], x, y);
    mNonplayers.push_back( aNpc ); // owned
    mRenderables.push_back( aNpc ); // not owned
    mNonplayers[mNonplayers.size()-1]->SetFacing( (sm_direction)ATOI(node->attr["facing"]) );
  }


  
  // initialize player
  // - get initial player position
  XMLNode* ps = mpCurLevel->getElementByTagName("playerStart");
  if(!ps ) {
    cerr << "Error!  Could not find <playerStart> element in first <conference> element" << endl;
    exit(-1);
  }
  // - get player sprite
  if(mSprites.find("player-sprite") == mSprites.end())  { 
    cerr << "Error!  Could not find player-sprite <sprite> in XML" << endl; 
    exit(-1); 
  }
  // - create player
  mpPlayer = new Player(mSprites["player-sprite"],
                  ATOI(ps->attr["x"]),
                  ATOI(ps->attr["y"]));
  // TODO: - ensure that facing is a valid value of sm_direction (and not sm_dir_none)
  mpPlayer->SetFacing( (sm_direction)ATOI(ps->attr["facing"]) );
  mRenderables.push_back(mpPlayer); // not owned

  InitScene();
}

void Game::InitSprites()
{
  // set up sprite resources
  vector<XMLNode*> spriteNodes;
  // TODO: this doesn't require a sprite to be in the <resources/> section
  int numSprites = mpConfig->getElementsByTagName("sprite", &spriteNodes);
  if(!numSprites) {
    cerr << "Error!  Could not find any <sprite> elements n wad.xml" << endl;
    exit(2004);
  }
  for(NodeIter spriteIt = spriteNodes.begin(); spriteIt != spriteNodes.end(); ++spriteIt) {
    XMLNode* node = *spriteIt;
    Sprite* sprite;

    if(node->id().length() == 0) { cerr << "Error!  Sprite id is blank" << endl; exit(2005); }
    if(node->attr["src"].length() == 0) { cerr << "Error!  Sprite '" << node->id() << "' does not have a src attribute" << endl; exit(2006); }
    if(node->attr["shadow"].length() == 0) {
      sprite = new Sprite(node->attr["src"].c_str());
    } else {
      sprite = new Sprite(node->attr["src"].c_str(), node->attr["shadow"].c_str());
    }
    mSprites[node->id()] = sprite;
  }
}

void Game::InitScene()
{
  mpScene->InitScene();

  mColMap.cellInPixels = 8; // each square represents an 8x8 pixel region
  mColMap.width = ATOI(mpCurLevel->attr["width"])/mColMap.cellInPixels;
  mColMap.height = ATOI(mpCurLevel->attr["height"])/mColMap.cellInPixels;

  crunchCollisionMap(mpScene->CollisionMap);
}

/*
 * Return the pixel value at (x, y)
 * NOTE: The surface must be locked before calling this!
 * (from the SDL docs at file:///usr/share/doc/packages/SDL-devel/html/guidevideo.html#AEN90)
 */
Uint32 getpixel(SDL_Surface *surface, int x, int y)
{
  int bpp = surface->format->BytesPerPixel;
  /* Here p is the address to the pixel we want to retrieve */
  Uint8 *p = (Uint8 *)surface->pixels + y * surface->pitch + x * bpp;

  switch(bpp) {
    case 1:
      return *p;

    case 2:
      return *(Uint16 *)p;

    case 3:
      if(SDL_BYTEORDER == SDL_BIG_ENDIAN)
        return p[0] << 16 | p[1] << 8 | p[2];
      else
        return p[0] | p[1] << 8 | p[2] << 16;

    case 4:
      return *(Uint32 *)p;

    default:
      return 0;       /* shouldn't happen, but avoids warnings */
  }
}

int Game::crunchCollisionMap(SDL_Surface *s) {
  int size = mColMap.width * mColMap.height;
  mColMap.map = new TouchableObject*[size];
  cout << "Crunching: " << hex;
  for (int ix=0, iy=0; (ix<s->w) && (iy<s->h); ix++,iy++) {
    cout << getpixel(s,ix, iy) << " ";
  }

  // ensure that s->w and s->h correspond to the same size as mColMap.map
  if ((mColMap.width * mColMap.cellInPixels != s->w) || (mColMap.height * mColMap.cellInPixels != s->h)) {
    cout << "Error: mColMap dimensions don't match image dimensions mColMap.width:" << mColMap.width << " s->w: " << s->w << " mColMap.height: " << mColMap.height << " s->h: " << s->h << endl;
    exit(-1);
  }

  // We read the collisionMap one cell at a time. So we start at (0,0) then read all pixels up to (8,8). This determines one entry in mColMap.map. After that we move on to the cell at (8,0) to (16,8)
  for (int cx=0; cx < mColMap.width; cx++) {
    for (int cy=0; cy < mColMap.height; cy++) {
      int pixelCount=0;
      // read in one cell from the full scale map
      for (int ix=0; ix < mColMap.cellInPixels; ix++) {
        for (int iy=0; iy < mColMap.cellInPixels; iy++) {
          // TODO: this is just the colour that the Gimp chose for transparent in this image but I think there's an alpha channel that could be used instead
          // get the current pixel
          if (getpixel(s, (cx * mColMap.cellInPixels) + ix,  (cy * mColMap.cellInPixels) + iy)== 0) {
          } else {
            pixelCount++;
          }
        }
      }
      // compute the map value from the full scale map
      if (pixelCount < 32) {
        mColMap.map[cx + cy * mColMap.width]=NULL;
      } else {
        mColMap.map[cx + cy * mColMap.width]=&theWall;
      }
    }
  }
  cout << dec << endl;

  cout << "Collision Map: " << endl;
  for (int iy=0; iy < mColMap.height; iy++) {
    for (int ix=0; ix < mColMap.width; ix++) {
      if (mColMap.map[ix + iy * mColMap.width]) {
        cout << "X";
      } else {
        cout << " ";
      }
    }
    cout << endl;
  }
}

// primarily what this is does is update the time left
// and display to the status area
void Game::Update(Uint8* keystate)
{
  if(IsGameOver()) { return; }

  if(mTimeLeftInSec > 0) {
    int oldTime = mTimeLeftInSec;
    mTimeLeftInSec = TIME_LEFT - (SDL_GetTicks() - mStartTimeInMs)/1000;
    if(mTimeLeftInSec != oldTime) {
//    cout << "Time Left (s) = " << mTimeLeftInSec << " seconds." << endl;
    }
  }

  // keyboard controls:
  // directions:
  //  w,a,d,s or up, left, down, right arrows or num pad equiv
//  static sm_direction mpPlayer->Facing = sm_dir_none;
  // if we want the player to just keep moving until they change direction, then this can be set to true or removed
  // I kind of like that - it's more like pacman
  bool moveKeyPressed=false; 
  
  // save the previous position of the player
  mpPlayer->prevX = mpPlayer->posX;
  mpPlayer->prevY = mpPlayer->posY;

  if (keystate[SDLK_w] || keystate[SDLK_UP] || keystate[SDLK_KP8]) {
    mpPlayer->SetFacing(sm_dir_up);
    moveKeyPressed = true;
  } 
  if (keystate[SDLK_a] || keystate[SDLK_LEFT] || keystate[SDLK_KP4]) {
    mpPlayer->SetFacing(sm_dir_left);
    moveKeyPressed = true;
  } 
  if (keystate[SDLK_s] || keystate[SDLK_DOWN] || keystate[SDLK_KP2]) {
    mpPlayer->SetFacing(sm_dir_down);
    moveKeyPressed = true;
  } 
  if (keystate[SDLK_d] || keystate[SDLK_RIGHT] || keystate[SDLK_KP6]) {
    mpPlayer->SetFacing(sm_dir_right);
    moveKeyPressed = true;
  }

  if (mpPlayer->GetFacing() != sm_dir_none) {
//    cout << "try to move " << mpPlayer->Facing << endl;
  }

  if (moveKeyPressed) {

    // set up SDL_Rects for use with CDR
    SDL_Rect curPos, newPos;

    // current position and w/h based on player
    // TODO: make it part of Character class eventually, remove hard-coded 20
    curPos.x = mpPlayer->posX; curPos.y = mpPlayer->posY;
    curPos.w = PLAYER_WIDTH; curPos.h = PLAYER_HEIGHT;

    // new Pos is adjusted by facing and speed
    newPos.x = curPos.x; newPos.y = curPos.y;
    if(mpPlayer->GetFacing() == sm_dir_up) { newPos.y -= mpPlayer->speed; }
    if(mpPlayer->GetFacing() == sm_dir_left) { newPos.x -= mpPlayer->speed; }
    if(mpPlayer->GetFacing() == sm_dir_down) { newPos.y += mpPlayer->speed; }
    if(mpPlayer->GetFacing() == sm_dir_right) { newPos.x += mpPlayer->speed; }

    CollisionEvent event;
    moveRectWithCdr( mColMap, curPos, newPos, event );
    if(event.result == sm_cdr_success || event.result == sm_cdr_partial_move) {
      //cout << "Was able to move, adjusting player now" << endl;
      mpPlayer->posX = event.finalX; mpPlayer->posY = event.finalY;
    }
    else if(event.result == sm_cdr_failure) {
      cerr << "CDR failure" << endl;
    }
    else { // could not move
      cout << "BUMP" << endl;
    }

    // TO DO: now react if there was a collision
    if(event.result == sm_cdr_partial_move || event.result == sm_cdr_cannot_move) {
      // only need to do something if we did collide and it was not a wall 
      if(event.cdrObject  && event.cdrObjectType != sm_cdo_wall) {
        // if swag, add the points, remove the swag from Game and from colmap and from rendering collection
        // if NPC...
      }
    }
  }

  // TO DO: This should never need to be called now (CDR takes care of map bounds checking)
  //        So once I'm convinced of that, remove this call and the function
  // Even though we do cdr now, we still must do this 
  // (because the map is an array, not a double array)
  int correctedX, correctedY;
  if (boundsCheck(mpPlayer->posX, mpPlayer->posY, correctedX, correctedY)) {
    cerr << "CDR: ERROR.  boundsCheck() was called." << endl;
    cout << "CDR: boundsCheck(" << mpPlayer->posX << "," << mpPlayer->posY << "," <<
            correctedX << "," << correctedY << ") returned non-zero, hit edge of map!" << endl;
    mpPlayer->posX = correctedX;
    mpPlayer->posY = correctedY;
  }

  // check for collision with swag
  bool repeat=false;
  do {
    for (vector<Swag *>::iterator swagIt = mSwaggage.begin(); swagIt != mSwaggage.end(); ++swagIt) {
      if (collisionTest(mpPlayer->posX, mpPlayer->posY, (*swagIt)->posX, (*swagIt)->posY)) {
        mpPlayer->Points += (*swagIt)->getPoints();
        // NOTE: remove it from mRenderables before we delete it so we don't get a segfault!
        mRenderables.erase(std::find(mRenderables.begin(), mRenderables.end(), (*swagIt)));
        delete (*swagIt);
        mSwaggage.erase(swagIt);
        if(mSwaggage.size() > 0) { repeat = true; }
        else {
          // end of game (for now), collect 5 bonus points for each second left
          while(mTimeLeftInSec > 0) {
            mpPlayer->Points += 5;
            --mTimeLeftInSec;
            DrawScreen();
          }
        }
        break;
      }
    }
    repeat = false;
  } while (repeat);

  DrawScreen();
}

void Game::DrawScreen()
{
  mpDisplay->LockScreen();

  // commit to screen
  // set up to redraw
  DrawScene();
  // draw the floor
  DrawFloor();

  // draw shadows for all TouchableObjects
  for (int idx = 0; idx < mRenderables.size(); ++idx) {
    mRenderables[idx]->Draw(mpDisplay->getScreen(), mRenderables[idx]->getSpeed(), true);
  }

  // draw the map
  DrawBackground();

  // draw all TouchableObjects
  for (int idx = 0; idx < mRenderables.size(); ++idx) {
    mRenderables[idx]->Draw(mpDisplay->getScreen(), mRenderables[idx]->getSpeed(), false);
  }

  // TODO: only update when i need to
  DrawStatus();

  // flip the screen now that all rendering is complete 
  mpDisplay->UnlockScreen();
  mpDisplay->Flip();
}

void Game::DrawScene()
{
  // blit in layers,
  //  1. background
  //  2. map and status graphics
  //  3. sprites

  // temporary background - two rectangles against black for now 
  SDL_Surface* scr = mpDisplay->getScreen();
  SDL_Rect rcMap = { MAP_POS_X, MAP_POS_Y, MAP_SIZE_X, MAP_SIZE_Y };
  SDL_Rect rcStatus = { STATUS_POS_X, STATUS_POS_Y, STATUS_SIZE_X, STATUS_SIZE_Y };
  SDL_FillRect(scr, NULL, SDL_MapRGB(scr->format, 0, 0, 0)); // black
  SDL_FillRect(scr, &rcStatus, SDL_MapRGB(scr->format, 192, 255, 192)); // lt green
}

void Game::DrawBackground()
{
  if(mpScene->Background) {
    SDL_BlitSurface(mpScene->Background, NULL, mpDisplay->getScreen(), &mpScene->MapRect);
  }
}

void Game::DrawFloor()
{
  if(mpScene->Floor) {
    SDL_BlitSurface(mpScene->Floor, NULL, mpDisplay->getScreen(), &mpScene->MapRect);
  }
}

void Game::DrawStatus()
{
  if(!mpDisplay || !mpPlayer) { return; }

  char szTimeStr[30];
  int time_x = STATUS_POS_X + 20; int time_y = STATUS_POS_Y + 50;
  SDL_Color black = {0,0,0,255}; 
  SDL_Color time_color = {0,0,0,255}; // black by default
  if(mTimeLeftInSec > 0) {
    sprintf(szTimeStr, "Time Left: %d:%02d", mTimeLeftInSec/60, mTimeLeftInSec%60);
  }
  else {
    sprintf(szTimeStr, "Game over, chump!");
    time_color.r = 255; // red
  }

  char szPlayerStr[30];
  int player_x = STATUS_POS_X + 10; int player_y = STATUS_POS_Y + 120;
  sprintf(szPlayerStr, "Pos: (%d,%d)", mpPlayer->posX, mpPlayer->posY);

  char szPlayerScore[20];
  int player_points_y = player_y + 20;
  sprintf(szPlayerScore, "Points: %d", mpPlayer->Points);

  char szLocation[40];
  sprintf(szLocation, "(%s)", mpCurLevel->attr["location"].c_str());

  int cx = STATUS_POS_X + (STATUS_SIZE_X/2);

  // rendering calls go here
  mpDisplay->SetCurrentFont("resources/fonts/VeraBd.ttf", 14);
  mpDisplay->DrawText(mpCurLevel->attr["name"].c_str(), black, 
                      cx, STATUS_POS_Y + 5, sm_disp_centerh);
  mpDisplay->SetCurrentFont("resources/fonts/Vera.ttf", 12);
  mpDisplay->DrawText(szLocation, black, 
                      cx, STATUS_POS_Y + 25, sm_disp_centerh);

  mpDisplay->SetCurrentFont("resources/fonts/Vera.ttf", 14);
  mpDisplay->DrawText(szTimeStr, time_color, cx, time_y, sm_disp_centerh);
  mpDisplay->DrawText(szPlayerStr, time_color, player_x, player_y);
  mpDisplay->DrawText(szPlayerScore, time_color, player_x, player_points_y);
}

// collision detection and reaction for the bounds of the map
// performs a bounds check with dimensions of the map
// returns 0 if check passes, 1 if correctedX, correctedY
// have changed with new values for position
// 547,448,544,448
int Game::boundsCheck(int x, int y, int &fixedX, int &fixedY) {
  int result = 0;
  if (x > PLAYER_POS_MAX_X) {
    fixedX = PLAYER_POS_MAX_X;
    fixedY = y;
    result = -1;
  }
  else if (x < PLAYER_POS_MIN_X) {
    fixedX = PLAYER_POS_MIN_X;
    fixedY = y;
    result = -1;
  }
  if (y > PLAYER_POS_MAX_Y) {
    fixedX = x;
    fixedY = PLAYER_POS_MAX_Y;
    result = -1;
  }
  else if (y < PLAYER_POS_MIN_Y) {
    fixedX = x;
    fixedY = PLAYER_POS_MIN_Y;
    result = -1;
  }
  return result;
}

// tests a 12x12 box in the middle of a box with top left at x1, y1 against a box with top left x2, y2
// based on SDL_CollideBoundingBox() but not rects
int Game::collisionTest(int x1, int y1, int x2, int y2) {
  int w = PLAYER_WIDTH, h = PLAYER_HEIGHT;
//  x1+=6; x2+=6;
//  y1+=6; y2+=6;
  if(x1 + w < x2) return 0;
  if(x2 + w < x1) return 0;

  if(y1 + h < y2) return 0;
  if(y2 + h < y1) return 0;

  return 1;
}

void Game::removeTOFromColMap(TouchableObject* o) {
  if( this->mColMap.map == NULL ) {
    cerr << "Error!  Colmap was NULL" << endl;
    return;
  }
  cerr << "TO DO: Implement Game::removeTOFromColMap()" << endl;
}

void Game::addTOToColMap(TouchableObject* o) {
  if( this->mColMap.map == NULL ) {
    cerr << "Error!  Colmap was NULL" << endl;
    return;
  }
  cerr << "TO DO: Implement Game::addTOToColMap()" << endl;
}
